/**
 * 
 */
package connectionpool.internal;


import java.sql.Connection;
import java.sql.SQLException;
import java.util.NoSuchElementException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import connectionpool.ConnectionPool;



/*****************************************************************************
 * @author amahmood
 * 
 *         Uses Connection Manager to create a connection.
 *         Maintains a list of leased connection and a list of available
 *         connections.
 *         If the pool is full. it waits for the connection to be available
 *         before the
 *         timeout expires. Throws exception otherwise.
 ****************************************************************************/
public class ConnectionPoolImpl implements ConnectionPool
{

    private int timeOutInSecs = 5;
    private BlockingQueue<Connection> leasedConnections = null;
    private BlockingQueue<Connection> connectionPool = null;
    private ConnectionManager connectionManager = null;

    private boolean debug = false;

    /***************************************************************************
     * @param maxPoolSize
     * @param connectionManager
     ****************************************************************************/
    public ConnectionPoolImpl(int maxPoolSize, ConnectionManager connectionManager)
    {
        CommonUtils.checkPreConditionNotNull("connectionManager", connectionManager);
        CommonUtils.checkPreConditionGreaterThanZero("maxPoolSize", maxPoolSize);

        this.connectionManager = connectionManager;
        leasedConnections = new ArrayBlockingQueue<Connection>(maxPoolSize);
        connectionPool = new ArrayBlockingQueue<Connection>(maxPoolSize);
    }

    /******************************************************************
     * (non-Javadoc)
     * 
     * @see java.lang.Object#finalize()
     *****************************************************************/
    @Override
    protected void finalize() throws Throwable
    {
        super.finalize();

        while (!leasedConnections.isEmpty())
        {
            try
            {
                Connection connection = leasedConnections.remove();
                connection.close();
            }
            catch (Exception e)
            {
                // do nothing;
            }
        }

        while (!connectionPool.isEmpty())
        {
            try
            {
                Connection connection = connectionPool.remove();
                connection.close();
            }
            catch (Exception e)
            {
                // do nothing;
            }
        }
    }

    /****************************************************************************
     * (non-Javadoc)
     * 
     * @see connectionpool.ConnectionPool#getConnection()
     ****************************************************************************/
    public Connection getConnection() throws SQLException
    {
        if (debug)
            System.out.println("Got Leasing request from " + Thread.currentThread().getName());

        Connection out = leaseConnectionFromPool();

        if (out == null)
        {
            if (isLeaseFull())
            {
                out = waitAndGetConnectionFromPool();

                if (out == null)
                    throw new SQLException("No connection available. Try again later.");
            }
            else
            {
                /*
                 * if a thread 1 is in releaseConnection and thread 2 is in
                 * getConnection then thread 2 can access this method based
                 * on the
                 * condition that pool is empty. But meanwhile thread 1 has
                 * released the
                 * connection so it is possible that when thread 2 comes
                 * here pool may
                 * not be empty. this check avoid such situation.
                 */
                synchronized(this)
                {
                     if (this.connectionPool.isEmpty())
                            out = leaseNewConneciton();
                     else
                     {
                         out = leaseConnectionFromPool();
    
                         if (out == null)
                             throw new IllegalStateException("Internal error. Unable to create connection object.");
                     }
                }
            }
        }

        return out;
    }

    /*****************************************************************************
     * @return
     ****************************************************************************/
    private boolean isLeaseFull()
    {
        return leasedConnections.remainingCapacity() == 0;

    }

    /*****************************************************************************
     * @return
     ****************************************************************************/
    private Connection leaseConnectionFromPool() throws SQLException
    {
        Connection out = null;

        try
        {
            // get a connection which is still valid
             out = connectionPool.remove();
             
             if (out!=null && !out.isClosed())
             {
                leasedConnections.add(out);

                if (debug)
                    System.out.println("Leased " + out.toString() + " to " + Thread.currentThread().getName());
            }
             else
                 out = null;
        }
        catch (NoSuchElementException e)
        {
            // do nothing ignore exception
            System.out.println("Ignoring: Pool is empty. " + e.getMessage());
        }

        return out;
    }

    /*****************************************************************************
     * @return
     * @throws SQLException
     ****************************************************************************/
    private Connection leaseNewConneciton() throws SQLException
    {
        Connection out = null;
       
        out = this.connectionManager.getConnection();
        this.leasedConnections.add(out);

        if (debug)
            System.out.println("Leased " + out.toString() + " to " + Thread.currentThread().getName());

        return out;
    }

    /* ***************************************************************************
     * (non-Javadoc)
     * 
     * @see
     * opower.connectionpool.ConnectionPool#releaseConnection(java.sql.Connection
     * )
     * *************************************************************************
     * *
     */
    public void releaseConnection(Connection con) throws SQLException
    {
        if (con == null)
            return;

        if (debug)
            System.out.println("Received " + con.toString() + " from " + Thread.currentThread().getName());

        synchronized (this)
        {
            if (leasedConnections.remove(con))
                connectionPool.add(con);
        }
    }

    /*****************************************************************************
     * @param value
     ****************************************************************************/
    public void setDebug(boolean value)
    {
        this.debug = value;
    }

    /*****************************************************************************
     * @return
     ****************************************************************************/
    private Connection waitAndGetConnectionFromPool()
    {
        Connection out = null;

        try
        {
            out = connectionPool.poll(timeOutInSecs, TimeUnit.SECONDS);

            if (out != null)
            {
                leasedConnections.add(out);

                if (debug)
                    System.out.println("Leased " + out.toString() + " to " + Thread.currentThread().getName());
            }
        }
        catch (InterruptedException e)
        {
            // ignore exception;
            System.out.println("Ignoring: Timeout. No connectin is avaiable. " + e.getMessage());
        }

        return out;
    }

}
